Vue 3 Introduction By Example

Vue 3 was released back in September so we've had some time to kick the tires on it now. It brings a lot of new features and also a lot of breaking changes. But, a lot of the breaking changes lay the groundwork for better tooling in the future. In this post we'll introduce Vue 3 by building a weather application using some of the new features.

Composition API

Getting Started

The Composition API is probably the biggest new addition. It's similar to React Hooks. It's a way to share logic between components by writing them in a functional manner. It will also better Typescript compatibility in Vue 3.

To start using the Composition API (with Typescript) we'll call the defineComponent function and return it from our Single File Component. That function takes an object representing a Vue component. Inside of that we'll implement a setup function.

The setup function is how you start using the Composition API. The function returns an object containing any values you want to use.

Reactivity with ref and computed

The old way of writing components is now called the Options API. In the Options API reactive objects could be created by returning them from the data or computed fields in the component.

In the Composition API you implement reactive objects by calling ref or computed.

ref corresponds to objects returned from data. The function returns reactive objects that can be updated in other places.

computed works the same way as the Options API. You pass in a function, whenever a reactive dependency in that function changes the function is run and the value is updated.

To start our weather app we'll make a component with defineComponent and create a reactive variable with ref that represents the weekly forecast. We'll return this value from the setup function so we can access it in the template.

<!-- Forecast.vue -->
<template>
  <div class="forecast">
    <daily-weather
      v-for="forecast of forecasts"
      :key="forecast.name"
      :forecast="forecast"
    />
  </div>
</template>

<script lang="ts">
  import { defineComponent } from 'vue'

  export default defineComponent({
    setup() {
      /** Weather forecasts for the week */
      const forecasts = ref<Array<Forecast>>([])

      return { forecasts }
    },
    components: { DailyWeather },
  })
</script>

Plugins

Provide and Inject

Many Vue Plugins in Vue 2 would extend the Vue object. For example, Vuex adds a the $store value to the Vue object.

The Composition API is functional, it doesn't have the same this context as the Options API. That means we can't access values from plugins that extend the Vue object.

We'll use the provide and inject to access plugin values in the Composition API. These functions allow for dependency injection. We define a value somewhere up the component tree with provide and use it in any of it's child components with inject.

Location Plugin

For our weather app we need a way to get the user's current location. We'll make a plugin that calls the HTML5 Geolocation API to get the user's current location.

In the install function for the plugin we'll provide it at the app level. We'll also add a function to use the provided value in components. That's the recommended way to do it as opposed to calling inject in the app component.

In Vue 2 plugins would be provided with the Vue instance to the install function of the plugin. Now it provides an App instance. As you can see in the example above, instead of extending the Vue prototype we add a property to app.config.globalProperties. This is the same as Vue.prototype. The app object also has a provide object. This is how we provide the location function at the top level.

//location-plugin.ts

/**
 * Get the users current location using the HTML5 GeoLocation API.
 *  We wrap it in a promise to use with async/await.
 */
function getLocation(): Promise<Position> {
  return new Promise((resolve, reject) => {
    window.navigator.geolocation.getCurrentPosition(
      position => {
        resolve(position) // Resolve with location. location can now be accessed in the .then method.
      },
      err => reject(err) // Reject with err. err can now be accessed in the .catch method.
    )
  })
}

/** Symbol to access the location function */
const locationSymbol = Symbol('location')

const LocationPlugin: Plugin = {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  install(app, options) {
    // eslint-disable-next-line no-param-reassign
    app.config.globalProperties.$location = getLocation

    app.provide(locationSymbol, getLocation)
  },
}

/** Type of the location function */
type LocationFunction = typeof getLocation

/** Function to use the location function from the API */
export function useLocation() {
  /** Function to get the geolocation */
  const locationFunction: LocationFunction | undefined = inject(locationSymbol)

  if (!locationFunction) {
    throw new Error('Could not get inject LocationPlugin')
  }

  return locationFunction
}

export default LocationPlugin

Using the Plugin

Using plugins in somewhat similar in Vue 3. Instead of calling Vue.use and then instantiating the Vue instance you need to create the app first with createApp. Then you call use on the object returned from createApp. After that, call mount to instantiate the app.

//main.ts

import { createApp } from 'vue'
import App from './App.vue'
import LocationPlugin from './location-plugin/location-plugin'

const app = createApp(App)

// Use the location plugin we wrote
app.use(LocationPlugin)

app.mount('#app')

Now we have a location function we can call to get the users location.

Lifecycle Hooks

In the Options API lifecycle hooks such as onMounted are properties of the component object. In the Composition API they're, you guessed it, functions!

We want to load the weather forecast from the NWS Weather API when the Forecast component is loaded. Normally this is done in the onMounted hook. We'll call the onMounted function, which takes a function as a parameter. We need the user's location to get the current forecast so we'll use our plugin.

<template>
  <div class="forecast">
    <daily-weather
      v-for="forecast of forecasts"
      :key="forecast.name"
      :forecast="forecast"
    />
  </div>
</template>

<script lang="ts">
  import { defineComponent, inject, onMounted, ref } from 'vue'
  import fetchWeather, { Forecast } from '../fetch_weather'
  import DailyWeather from './DailyWeather.vue'
  import { useLocation } from '../location-plugin/location-plugin'

  export default defineComponent({
    setup() {
      /** Weather forecasts for the week */
      const forecasts = ref<Array<Forecast>>([])

      const getLocation = useLocation()

      // Register a function to be called in the onMounted lifecycle
      onMounted(async () => {
        if (getLocation) {
          const location = await getLocation()
          forecasts.value = await fetchWeather(location)
        }
      })

      return { forecasts }
    },
    components: { DailyWeather },
  })
</script>

Fragments

Another quality of life improvement to Vue 3 is multi-root components. This means components can have more than one root node now. Instead of wrapping all your components in a div you can just add them.

<!-- App.vue -->

<template>
  <page-header />
  <forecast />
</template>

<script lang="ts">
  import { defineComponent } from 'vue'
  import Forecast from './components/Forecast.vue'
  import PageHeader from './components/PagerHeader.vue'

  export default defineComponent({
    name: 'App',
    components: {
      Forecast,
      PageHeader,
    },
    setup() {
      return {}
    },
  })
</script>

Completed Code

You can see the completed code on Github at https://github.com/toastking/vue3-weather-example.